{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e740dfbe",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | default_exp _components.logger"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62377675",
   "metadata": {},
   "source": [
    "# Logger"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8efd915a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "\n",
    "import logging\n",
    "import logging.config\n",
    "from typing import *\n",
    "\n",
    "from fastkafka._components.helpers import true_after"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ab182545",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | include: false\n",
    "\n",
    "import time\n",
    "import unittest\n",
    "\n",
    "import pytest"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4a4dfedf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "\n",
    "# Logger Levels\n",
    "# CRITICAL = 50\n",
    "# ERROR = 40\n",
    "# WARNING = 30\n",
    "# INFO = 20\n",
    "# DEBUG = 10\n",
    "# NOTSET = 0\n",
    "\n",
    "should_suppress_timestamps: bool = False\n",
    "\n",
    "\n",
    "def suppress_timestamps(flag: bool = True) -> None:\n",
    "    \"\"\"Suppress logger timestamp\n",
    "\n",
    "    Args:\n",
    "        flag: If not set, then the default value **True** will be used to suppress the timestamp\n",
    "            from the logger messages\n",
    "    \"\"\"\n",
    "    global should_suppress_timestamps\n",
    "    should_suppress_timestamps = flag\n",
    "\n",
    "\n",
    "def get_default_logger_configuration(level: int = logging.INFO) -> Dict[str, Any]:\n",
    "    \"\"\"Return the common configurations for the logger\n",
    "\n",
    "    Args:\n",
    "        level: Logger level to set\n",
    "\n",
    "    Returns:\n",
    "        A dict with default logger configuration\n",
    "\n",
    "    \"\"\"\n",
    "    global should_suppress_timestamps\n",
    "\n",
    "    if should_suppress_timestamps:\n",
    "        FORMAT = \"[%(levelname)s] %(name)s: %(message)s\"\n",
    "    else:\n",
    "        FORMAT = \"%(asctime)s.%(msecs)03d [%(levelname)s] %(name)s: %(message)s\"\n",
    "\n",
    "    DATE_FMT = \"%y-%m-%d %H:%M:%S\"\n",
    "\n",
    "    LOGGING_CONFIG = {\n",
    "        \"version\": 1,\n",
    "        \"disable_existing_loggers\": False,\n",
    "        \"formatters\": {\n",
    "            \"standard\": {\"format\": FORMAT, \"datefmt\": DATE_FMT},\n",
    "        },\n",
    "        \"handlers\": {\n",
    "            \"default\": {\n",
    "                \"level\": level,\n",
    "                \"formatter\": \"standard\",\n",
    "                \"class\": \"logging.StreamHandler\",\n",
    "                \"stream\": \"ext://sys.stdout\",  # Default is stderr\n",
    "            },\n",
    "        },\n",
    "        \"loggers\": {\n",
    "            \"\": {\"handlers\": [\"default\"], \"level\": level},  # root logger\n",
    "        },\n",
    "    }\n",
    "    return LOGGING_CONFIG"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "251df829",
   "metadata": {},
   "source": [
    "Example on how to use **get_default_logger_configuration** function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6e725745",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'version': 1,\n",
       " 'disable_existing_loggers': False,\n",
       " 'formatters': {'standard': {'format': '%(asctime)s.%(msecs)03d [%(levelname)s] %(name)s: %(message)s',\n",
       "   'datefmt': '%y-%m-%d %H:%M:%S'}},\n",
       " 'handlers': {'default': {'level': 20,\n",
       "   'formatter': 'standard',\n",
       "   'class': 'logging.StreamHandler',\n",
       "   'stream': 'ext://sys.stdout'}},\n",
       " 'loggers': {'': {'handlers': ['default'], 'level': 20}}}"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# collapse_output\n",
    "\n",
    "get_default_logger_configuration()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "718c810d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'version': 1,\n",
       " 'disable_existing_loggers': False,\n",
       " 'formatters': {'standard': {'format': '%(asctime)s.%(msecs)03d [%(levelname)s] %(name)s: %(message)s',\n",
       "   'datefmt': '%y-%m-%d %H:%M:%S'}},\n",
       " 'handlers': {'default': {'level': 20,\n",
       "   'formatter': 'standard',\n",
       "   'class': 'logging.StreamHandler',\n",
       "   'stream': 'ext://sys.stdout'}},\n",
       " 'loggers': {'': {'handlers': ['default'], 'level': 20}}}"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# | include: false\n",
    "\n",
    "expected = {\n",
    "    \"version\": 1,\n",
    "    \"disable_existing_loggers\": False,\n",
    "    \"formatters\": {\n",
    "        \"standard\": {\n",
    "            \"format\": \"%(asctime)s.%(msecs)03d [%(levelname)s] %(name)s: %(message)s\",\n",
    "            \"datefmt\": \"%y-%m-%d %H:%M:%S\",\n",
    "        }\n",
    "    },\n",
    "    \"handlers\": {\n",
    "        \"default\": {\n",
    "            \"level\": 20,\n",
    "            \"formatter\": \"standard\",\n",
    "            \"class\": \"logging.StreamHandler\",\n",
    "            \"stream\": \"ext://sys.stdout\",\n",
    "        }\n",
    "    },\n",
    "    \"loggers\": {\"\": {\"handlers\": [\"default\"], \"level\": 20}},\n",
    "}\n",
    "actual = get_default_logger_configuration()\n",
    "assert actual == expected\n",
    "actual"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f19186ba",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "\n",
    "logger_spaces_added: List[str] = []\n",
    "\n",
    "\n",
    "def get_logger(\n",
    "    name: str, *, level: int = logging.INFO, add_spaces: bool = True\n",
    ") -> logging.Logger:\n",
    "    \"\"\"Return the logger class with default logging configuration.\n",
    "\n",
    "    Args:\n",
    "        name: Pass the __name__ variable as name while calling\n",
    "        level: Used to configure logging, default value `logging.INFO` logs\n",
    "            info messages and up.\n",
    "        add_spaces:\n",
    "\n",
    "    Returns:\n",
    "        The logging.Logger class with default/custom logging configuration\n",
    "\n",
    "    \"\"\"\n",
    "    config = get_default_logger_configuration(level=level)\n",
    "    logging.config.dictConfig(config)\n",
    "\n",
    "    logger = logging.getLogger(name)\n",
    "    #     stack_size = len(traceback.extract_stack())\n",
    "    #     def add_spaces_f(f):\n",
    "    #         def f_with_spaces(msg, *args, **kwargs):\n",
    "    #             cur_stack_size = len(traceback.extract_stack())\n",
    "    #             msg = \" \"*(cur_stack_size-stack_size)*2 + msg\n",
    "    #             return f(msg, *args, **kwargs)\n",
    "    #         return f_with_spaces\n",
    "\n",
    "    #     if name not in logger_spaces_added and add_spaces:\n",
    "    #         logger.debug = add_spaces_f(logger.debug) # type: ignore\n",
    "    #         logger.info = add_spaces_f(logger.info) # type: ignore\n",
    "    #         logger.warning = add_spaces_f(logger.warning) # type: ignore\n",
    "    #         logger.error = add_spaces_f(logger.error) # type: ignore\n",
    "    #         logger.critical = add_spaces_f(logger.critical) # type: ignore\n",
    "    #         logger.exception = add_spaces_f(logger.exception) # type: ignore\n",
    "\n",
    "    #         logger_spaces_added.append(name)\n",
    "\n",
    "    return logger"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7fea37e9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | include: false\n",
    "\n",
    "assert type(get_logger(__name__)) == logging.Logger\n",
    "\n",
    "with pytest.raises(TypeError) as e:\n",
    "    get_logger()\n",
    "assert \"missing 1 required positional argument\" in str(e.value)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3c6a2ae0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "23-08-08 04:32:22.060 [INFO] __main__: hello\n",
      "23-08-08 04:32:22.061 [INFO] __main__: hello\n",
      "23-08-08 04:32:22.061 [INFO] __main__: hello\n"
     ]
    }
   ],
   "source": [
    "logger = get_logger(__name__)\n",
    "logger.info(\"hello\")\n",
    "logger = get_logger(__name__)\n",
    "logger.info(\"hello\")\n",
    "\n",
    "\n",
    "def f():\n",
    "    logger.info(\"hello\")\n",
    "\n",
    "\n",
    "f()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "390214db",
   "metadata": {},
   "source": [
    "Example on how to use **get_logger** function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "90767ccb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "23-08-08 04:32:22.067 [INFO] __main__: info\n",
      "23-08-08 04:32:22.067 [WARNING] __main__: Warning\n",
      "23-08-08 04:32:22.068 [ERROR] __main__: Error\n",
      "23-08-08 04:32:22.068 [CRITICAL] __main__: Critical\n"
     ]
    }
   ],
   "source": [
    "# collapse_output\n",
    "\n",
    "logger = get_logger(__name__)\n",
    "\n",
    "logger.debug(\"Debug\")\n",
    "logger.info(\"info\")\n",
    "logger.warning(\"Warning\")\n",
    "logger.error(\"Error\")\n",
    "logger.critical(\"Critical\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ede2ce1f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[INFO] __main__: info\n",
      "[WARNING] __main__: Warning\n",
      "[ERROR] __main__: Error\n",
      "[CRITICAL] __main__: Critical\n"
     ]
    }
   ],
   "source": [
    "# collapse_output\n",
    "\n",
    "suppress_timestamps()\n",
    "logger = get_logger(__name__)\n",
    "\n",
    "logger.debug(\"Debug\")\n",
    "logger.info(\"info\")\n",
    "logger.warning(\"Warning\")\n",
    "logger.error(\"Error\")\n",
    "logger.critical(\"Critical\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1e791150",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "\n",
    "\n",
    "def set_level(level: int) -> None:\n",
    "    \"\"\"Set logger level\n",
    "\n",
    "    Args:\n",
    "        level: Logger level to set\n",
    "    \"\"\"\n",
    "\n",
    "    # Getting all loggers that has either fastkafka or __main__ in the name\n",
    "    loggers = [\n",
    "        logging.getLogger(name)\n",
    "        for name in logging.root.manager.loggerDict\n",
    "        if (\"fastkafka\" in name) or (\"__main__\" in name)\n",
    "    ]\n",
    "\n",
    "    for logger in loggers:\n",
    "        logger.setLevel(level)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5db8d01f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "40\n",
      "[ERROR] __main__: This is an error\n"
     ]
    }
   ],
   "source": [
    "level = logging.ERROR\n",
    "\n",
    "set_level(level)\n",
    "\n",
    "# Checking if the logger is set back to logging.WARNING in dev mode\n",
    "print(logger.getEffectiveLevel())\n",
    "assert logger.getEffectiveLevel() == level\n",
    "\n",
    "logger.debug(\"This is a debug message\")\n",
    "logger.info(\"This is an info\")\n",
    "logger.warning(\"This is a warning\")\n",
    "logger.error(\"This is an error\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b9c2d1b0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[INFO] __main__: something\n"
     ]
    }
   ],
   "source": [
    "# Reset log level back to info\n",
    "level = logging.INFO\n",
    "\n",
    "set_level(level)\n",
    "logger.info(\"something\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ce1b40e5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "int"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(logging.INFO)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04cf2ffb",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "\n",
    "\n",
    "def cached_log(\n",
    "    self: logging.Logger,\n",
    "    msg: str,\n",
    "    level: int,\n",
    "    timeout: Union[int, float] = 5,\n",
    "    log_id: Optional[str] = None,\n",
    ") -> None:\n",
    "    \"\"\"\n",
    "    Logs a message with a specified level only once within a given timeout.\n",
    "\n",
    "    Args:\n",
    "        self: The logger instance.\n",
    "        msg: The message to log.\n",
    "        level: The logging level for the message.\n",
    "        timeout: The timeout duration in seconds.\n",
    "        log_id: Id of the log to timeout for timeout time, if None, msg will be used as log_id\n",
    "\n",
    "    Returns:\n",
    "        None\n",
    "    \"\"\"\n",
    "    if not hasattr(self, \"_timeouted_msgs\"):\n",
    "        self._timeouted_msgs = {}  # type: ignore\n",
    "        \n",
    "    key = msg if log_id is None else log_id\n",
    "\n",
    "    if msg not in self._timeouted_msgs or self._timeouted_msgs[key]():  # type: ignore\n",
    "        self._timeouted_msgs[key] = true_after(timeout)  # type: ignore\n",
    "\n",
    "        self.log(level, msg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "65185f0f",
   "metadata": {},
   "outputs": [],
   "source": [
    "with unittest.mock.patch(\"logging.Logger.log\") as mock:\n",
    "    for i in range(3 * 5 - 2):\n",
    "        cached_log(logger, \"log me!\", level=logging.INFO, timeout=1)\n",
    "        time.sleep(0.2)\n",
    "\n",
    "    assert mock.call_args_list == [unittest.mock.call(20, \"log me!\")] * 3"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "python3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
